package com.gframework.mybatis.util.generator.main;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.EventPublishingRunListener;
import org.springframework.core.env.Environment;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.StringUtils;

import com.gframework.mybatis.util.generator.core.api.GeneratorMybatis;
import com.gframework.mybatis.util.generator.core.conf.Configuration;
import com.gframework.mybatis.util.generator.interceptor.DaoInterceptor;
import com.gframework.mybatis.util.generator.interceptor.EntityInterceptor;
import com.gframework.mybatis.util.generator.interceptor.XmlInterceptor;
import com.gframework.util.GUtils;

/**
 * 此类会读取classpath:application.yml或application.yaml或application.properties中的数据库配置进行生成。
 * 
 * @since 1.0.0
 * @author Ghwolf
 */
public class GeneratorCodeByEnvironment {

	/**
	 * 要生成的表名称正则表达式
	 */
	private String tableNameReg;
	/**
	 * 本次生成代码所属模块名称
	 */
	private String moduleName;
	/**
	 * 本次生成的代码所在包
	 */
	private String basePackage;

	
	// need to set param
	/**
	 * 数据库schema信息，mysql为库名称，oracle为用户名
	 */
	private String schema;
	private String[] ignoreTableNamePrefix = new String[0];
	private String dbUrlKey ;
	private String usernameKey ;
	private String passwordKey ;

	private GeneratorCodeByEnvironment(String tableNameReg, String moduleName, String basePackage) {
		this.tableNameReg = tableNameReg;
		this.basePackage = basePackage;
		if (moduleName.contains(".")) {
			int endPointIndex = moduleName.lastIndexOf('.');
			String appendPackage = moduleName.substring(0, endPointIndex);
			this.basePackage += "." + appendPackage;
			this.moduleName = moduleName.substring(endPointIndex + 1);
		} else {
			this.moduleName = moduleName;
		}
	}

	/**
	 * 获取springboot环境配置对象
	 */
	private Environment getEnvironment() throws Exception{
		StackTraceElement[] stackTraces = new Exception().getStackTrace();
		
		ApplicationArguments applicationArguments = new DefaultApplicationArguments();
		ObjenesisStd std = new ObjenesisStd();
		Object listeners = std.newInstance(Class.forName("org.springframework.boot.SpringApplicationRunListeners"));
		Field listenersField = listeners.getClass().getDeclaredField("listeners");
		listenersField.setAccessible(true);
		
		SpringApplication app = new SpringApplication(Class.forName(stackTraces[stackTraces.length - 1].getClassName()));
		listenersField.set(listeners, Arrays.asList(new EventPublishingRunListener(app,new String[0])));
		
		Method prepareEnvironmentMethod = SpringApplication.class.getDeclaredMethod("prepareEnvironment", listeners.getClass(),ApplicationArguments.class);
		prepareEnvironmentMethod.setAccessible(true);
		return (Environment)prepareEnvironmentMethod.invoke(app, listeners,applicationArguments);
	}
	
	private void generator() throws Exception {
		Environment env = getEnvironment();
		String dburl = env.getProperty(dbUrlKey);
		String username = env.getProperty(usernameKey);
		String password = env.getProperty(passwordKey);

		if (dburl == null) {
			throw new NullPointerException("请检查springboot配置，是否存在数据源相关的配置！");
		}

		Configuration conf = new Configuration();

		if (StringUtils.startsWithIgnoreCase(dburl,"jdbc:oracle")) {
			handleOracle(conf, dburl, username);
		} else if (StringUtils.startsWithIgnoreCase(dburl,"jdbc:mysql") || StringUtils.startsWithIgnoreCase(dburl,"jdbc:mariadb")) {
			handleMysql(conf, dburl, username);
		} else if (StringUtils.startsWithIgnoreCase(dburl,"jdbc:sqlite")) {
			// Ignore
		} else {
			if (this.schema == null) {
				System.err.println("未知的数据库类型，无法获取schema(如无需可无视)，可能会导致表信息读取失败，请手动设置#setScema(String)：" + dburl);
			}
		}

		conf.setBasePackage(basePackage);
		conf.setJavaBeanInterceptor(new EntityInterceptor());
		conf.setJavaDaoInterceptor(new DaoInterceptor());
		conf.setXmlElementInterceptor(new XmlInterceptor());
		conf.setJdbc(dburl, username, password);
		conf.addSqlGeneratorPackage("com.gframework.mybatis.util.generator.gen");
		conf.setTable(null, this.schema == null ? null : this.schema.toUpperCase(), tableNameReg, moduleName);
		conf.setWriterableBean(true);
		conf.setWriterableDao(false);
		// conf.setWriterableXml(true);
		conf.setNoXml(true);
		conf.setCreateCatalogAndSchema(false);

		for (String pre : ignoreTableNamePrefix) {
			conf.addIgnoreTableNamePrefix(pre);
		}

		GeneratorMybatis gen = new GeneratorMybatis(conf);
		gen.generator();
	}

	private void handleMysql(Configuration conf, String dburl, String username) {
		conf.addJdbcUrlParam("remarks", "true");
		conf.addJdbcUrlParam("useInformationSchema", "true");
		if (this.schema == null) {
			int index = dburl.indexOf('?');
			if (index == -1) {
				int sIndex = dburl.lastIndexOf('/');
				this.schema = dburl.substring(sIndex + 1);
			} else {
				int sIndex = dburl.lastIndexOf('/', index);
				this.schema = dburl.substring(sIndex + 1, index);
			}
		}
	}

	private void handleOracle(Configuration conf, String dburl, String username) {
		// Oracle必须带这个才能有注释
		conf.addJdbcUrlParam("remarksReporting", "true");
		if (this.schema == null) {
			this.schema = username.toUpperCase();
		}
	}

	public static Config config() {
		return new Config();
	}

	public static class Config {

		private String dbUrlKey = "spring.datasource.url";
		private String usernameKey = "spring.datasource.username";
		private String passwordKey = "spring.datasource.password";
		private List<String> ignoreTableNamePrefix = new ArrayList<>();
		private String schema;
		private String basePackage = "com.gframework.biz";

		private Config() {
			this.ignoreTableNamePrefix.add("T_");
		}
		
		public Config run(String tableNameReg, String moduleName) throws Exception {
			return this.run(tableNameReg, moduleName, this.basePackage);
		}
		public Config run(String tableNameReg, String moduleName, String basePackage) throws Exception {
			GeneratorCodeByEnvironment g = new GeneratorCodeByEnvironment(tableNameReg,moduleName,basePackage);
			init(g);
			g.generator();
			System.out.println("生成完毕");
			return this ;
		}
		
		private void init(GeneratorCodeByEnvironment g){
			g.schema = this.schema ;
			g.dbUrlKey = this.dbUrlKey;
			g.usernameKey = this.usernameKey;
			g.passwordKey = this.passwordKey;
			// 处理忽略名称优先级，名称越长优先级越高
			this.ignoreTableNamePrefix.sort(null);
			Collections.reverse(this.ignoreTableNamePrefix);
			g.ignoreTableNamePrefix = this.ignoreTableNamePrefix.toArray(new String[this.ignoreTableNamePrefix.size()]);
		}

		/**
		 * 设置生成代码所在包路径
		 */
		public Config setBasePackage(String basePackage) {
			this.basePackage = basePackage;
			return this;
		}
		
		/**
		 * 设置数据库连接url在springboot配置文件中的key.
		 * <p>默认是：spring.datasource.url
		 */
		public Config setDbUrlKey(String dbUrlKey) {
			this.dbUrlKey = dbUrlKey;
			return this;
		}

		/**
		 * 设置数据库连接用户在springboot配置文件中的key.
		 * <p>默认是：spring.datasource.username
		 */
		public Config setUsernameKey(String usernameKey) {
			this.usernameKey = usernameKey;
			return this;
		}
		/**
		 * 设置数据库连接密码在springboot配置文件中的key.
		 * <p>默认是：spring.datasource.password
		 */
		public Config setPasswordKey(String passwordKey) {
			this.passwordKey = passwordKey;
			return this;
		}
		/**
		 * 设置 忽略的表名称前缀(不区分大小写)（在生成实体类名称等代码的时候，不会将忽略的内容给包含进去）.
		 * <p>此设置会覆盖已有的配置
		 * @param ignoreTableNamePrefix 忽略名称前缀 
		 */
		public Config setIgnoreTableNamePrefix(String ... ignoreTableNamePrefix) {
			this.ignoreTableNamePrefix.clear();
			return this.addIgnoreTableNamePrefix(ignoreTableNamePrefix);
		}
		/**
		 * 添加 忽略的表名称前缀(不区分大小写)（在生成实体类名称等代码的时候，不会将忽略的内容给包含进去）.
		 * @param ignoreTableNamePrefix 忽略名称前缀 
		 */
		public Config addIgnoreTableNamePrefix(String ... ignoreTableNamePrefix) {
			if (ignoreTableNamePrefix != null && ignoreTableNamePrefix.length != 0) {
				GUtils.map(ignoreTableNamePrefix,String :: toUpperCase);
				Collections.addAll(this.ignoreTableNamePrefix,ignoreTableNamePrefix);
			}
			return this;
		}
		/**
		 * 数据库schema信息，mysql为库名称，oracle为用户名.
		 * <p>
		 * 如果是自己不了解的数据库，可以查询相关资料了解此数据库的schmea信息，部分数据库无此信息可以不设置
		 */
		public Config setSchema(String schema) {
			this.schema = schema;
			return this;
		}

	}

}
